/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.editor.controller;

import offset.nodes.client.editor.controller.component.ComponentKit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;

import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Keymap;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLWriter;
import javax.swing.text.html.StyleSheet;

import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.model.DocumentContext;
import offset.nodes.client.editor.model.EditorDocument;
import offset.nodes.client.editor.model.EditorModel;
import offset.nodes.client.editor.view.EditorCaret;

public class EditorKit extends HTMLEditorKit {

    Editor editor;
    JEditorPane pane;
    ComponentKit componentKit;
    public static final String COPY_MARK_START = "<!--StartFragment-->";
    public static final String COPY_MARK_END = "<!--EndFragment-->";
    
    EditorViewFactory viewFactory = new EditorViewFactory();

    public EditorKit(Editor editor) {
        this.editor = editor;
        this.componentKit = createComponentKit();
    }

    public Editor getEditor() {
        return editor;
    }

    public ComponentKit getComponentKit() {
        return componentKit;
    }
    
    protected ComponentKit createComponentKit() {
        return new ComponentKit(editor);
    }

    protected Action getAction(String name, JEditorPane pane) {
        Action[] actions = pane.getActions();
        for (int i = 0; i < actions.length; i++) {
            if (name.equals((String) actions[i].getValue("Name")))
                return actions[i];
        }

        return null;
    }

    protected Parser getParser() {
        Parser parser = null;
        try {
            Class c = Class.forName("javax.swing.text.html.parser.ParserDelegator");
            parser = (Parser) c.newInstance();
        } catch (Throwable e) {
            return null;
        }

        return parser;
    }

    protected String readString(Reader in) throws IOException {
        StringBuilder sb = new StringBuilder();
        char[] buf = new char[1024];
        int count = 0;

        while ((count = in.read(buf)) >= 0) {
            sb.append(buf, 0, count);
        }

        return sb.toString();
    }

    /**
     * Used by the paste action. Filters the code for the relevant part between StartFragment and EndFragment
     * comments. This will be inserted to the container at pos, not just the body.
     *
     * @param in  the stream to read from
     * @param doc the destination for the insertion
     * @param pos the location in the document to place the
     *   content
     * @exception IOException on any I/O error
     * @exception BadLocationException if pos represents an invalid
     *   location within the document
     * @exception RuntimeException (will eventually be a BadLocationException)
     *            if pos is invalid
     */
    public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {

        if (doc instanceof HTMLDocument) {
           String input = readString(in);

           MarkedHTMLReader filter = new MarkedHTMLReader(input, (HTMLDocument) doc, pos);
            if (filter.useReader()) {
                filter.read();
                return;
            } else
                in = new StringReader(input);
        }

        super.read(in, doc, pos);
    }

    /**
     * Implement a more intelligent approach for paste action, if the clipboard
     * HTML includes marks and there is an ancestor, that can be used as
     * a container for the marked part.
     */
    class MarkedHTMLReader {
        String html;
        String marked;
        HTMLDocument doc;
        int pos;
        EditorModel model;
        Element container;

        public MarkedHTMLReader(String html, HTMLDocument doc, int insert) throws BadLocationException, IOException {
            this.model = new EditorModel(new DocumentContext(pane));
            this.html = html;
            this.doc = doc;
            this.pos = insert;
            // search for a structure container
            if (!hasMark())
                return;

            this.marked = getMarkedHTML(html);
            this.container = getMarkedElementContainer(insert);
            if (container == null) {
                marked = stripStructureElements(marked);
                container = model.getContainer(insert);
            }
        }

        public void read() throws BadLocationException, IOException {
            String first = model.getInnerHTML(container, container.getStartOffset(), pos);
            String second = model.getInnerHTML(container, pos, container.getEndOffset());
            
            doc.setInnerHTML(container, first + getMarkedHTML(html) + second);
        }

        public boolean hasMark() {
            if (html.indexOf(COPY_MARK_START) < 0
                    || html.indexOf(COPY_MARK_END) < 0) {
                return false;
            }

            return true;
        }

         public boolean useReader() {
            if (!hasMark() 
                    || container == null) {
                return false;
            }

            return true;
        }

        protected String getMarkedHTML(String html) {
            return html.substring(html.indexOf(COPY_MARK_START) + COPY_MARK_START.length(), html.indexOf(COPY_MARK_END));
        }

        /**
         * Optionally, strip all leading elements from the clipboard HTML, that did not have
         * a parent in the document
         *
         * @param html the clipboard HTML, potentially including structure elements without a parent in the document
         * @return the HTML without such structure elements
         */
        protected String stripStructureElements(String marked) {
            while (model.isStructureTag(model.getFirstTag(marked))) {
                marked = model.getInnerHTML(marked);
            }

            return marked;
        }

        /**
         * Determine the possible parent elements from the marked clipboard HTML.
         * Search up in the element hierarchy for the parent starting from the element at
         * insert position.
         *
         * @param insert the insert position
         * @return the container of the marked HTML, if found, else null.
         */
        protected Element getMarkedElementContainer(int insert) {
            Element current = doc.getParagraphElement(insert);
            return model.getStructureParent(current, model.getFirstTag(marked));
        }

    }

    /**
     * Used by the copy action. Adds StartFragment and EndFragment comments to the output
     * to mark the relevant part for insertion.
     *
     * @param out  the stream to write to
     * @param doc  the source for the write
     * @param pos  the location in the document to fetch the
     *   content
     * @param len  the amount to write out
     * @exception IOException on any I/O error
     * @exception BadLocationException if pos represents an invalid
     *   location within the document
     */
    public void write(Writer out, Document doc, int pos, int len)
            throws IOException, BadLocationException {

        if (doc instanceof HTMLDocument) {
            HTMLWriter w = new MarkingHTMLWriter(out, (HTMLDocument) doc, pos, len);
            w.write();
        } else
            super.write(out, doc, pos, len);
    }

    /**
     * Extend the HTMLWriter to include an MS StartFragment and EndFragment mark
     * in case of copy action.
     */
    class MarkingHTMLWriter extends HTMLWriter {
        EditorModel model;
        boolean skip = false;
        Element markedElement;

        public MarkingHTMLWriter(Writer w, HTMLDocument doc, int pos, int len) {
            super(w, doc, pos, len);
            this.model = new EditorModel(new DocumentContext(pane));
            this.markedElement = getMarkedElement(doc);
        }

        void writeLine(String string) throws IOException {
            write(string);
            writeLineSeparator();
        }

        protected boolean hasNoOrLeafChildren(Element e) {
            if (e.getElementCount() == 0)
                return true;

            for (int i = 0; i < e.getElementCount(); i++) {
                Element child = e.getElement(i);
                if (child.isLeaf()
                        || model.isArtificialElement(child))
                    return true;
            }

            return false;
        }

        /**
         * Return the common parent of the elements, that are
         * selected currently. The element shall be no leaf or artificial element
         * like "a".
         *
         * @param doc the document
         * @return the element
         */
        protected Element getMarkedElement(HTMLDocument doc) {
            Element start = doc.getParagraphElement(getStartOffset());
            Element end = doc.getParagraphElement(getEndOffset());

            int startDepth = getDepth(start);
            int endDepth = getDepth(end);

            if (startDepth > endDepth)
                start = up(start, startDepth - endDepth);
            else if (endDepth > startDepth)
                end = up(end, endDepth - startDepth);

            boolean found = false;
            while (start != null && end != null) {
                if (start == end
                        && !model.isArtificialElement(start)
                        && !start.isLeaf()) {
                    return start;
                }

                if (!model.isElementTag(start, HTML.Tag.BODY))
                    start = start.getParentElement();
                if (!model.isElementTag(end, HTML.Tag.BODY))
                    end = end.getParentElement();
            }

            return null;
        }
        
        /**
         * Return the ancestor element, which is "level"
         * elements above the element.
         * 
         * @param e the current element
         * @param level the number of levels to go up
         * @return the ancestor element
         */
        protected Element up(Element e, int level) {
            for (int i = 0; i < level && e != null; i++) 
                e = e.getParentElement();
            
            return e;
        }

        /**
         * Return the depth of the element, i.e. the
         * distance from the root element
         *
         * @param e the element
         * @return the number of levels from the root element
         */
        protected int getDepth(Element e) {
            int depth = 0;
            while (e != null) {
                depth++;
                e = e.getParentElement();
            }

            return depth;
        }

        /**
         * Determine the outer HTML including the mark string (mark start, mark end) for the element
         * that includes the selected elements and send the HTML to the writer.
         *
         * Skip elements below an already written element.
         *
         * @param elem
         * @throws IOException
         * @throws BadLocationException
         */
        protected void startTag(Element elem) throws IOException, BadLocationException {
            if (skip)
                return;
            
            if (elem == markedElement) {
                skip = true;

                StringBuilder buf = new StringBuilder();
                if (elem.getStartOffset() == getStartOffset())
                    buf.append(COPY_MARK_START);
                buf.append(model.getStartTag(elem));
                if (elem.getStartOffset() < getStartOffset()) {
                    buf.append(model.getInnerHTML(elem, elem.getStartOffset(), getStartOffset()));
                    buf.append(COPY_MARK_START);
                }
                buf.append(model.getInnerHTML(elem, getStartOffset(), getEndOffset()));
                if (elem.getEndOffset() - 1 > getEndOffset())
                    buf.append(COPY_MARK_END);
               if (elem.getEndOffset() - 1 > getEndOffset()) 
                    buf.append(model.getInnerHTML(elem, getEndOffset(), elem.getEndOffset()));
                buf.append(model.getEndTag(elem));
                if (elem.getEndOffset() - 1 == getEndOffset())
                    buf.append(COPY_MARK_END);
                
                 write(buf.toString());
            } else
                super.startTag(elem);
        }

        /**
         * Skip elements below an element already written by startTag.
         *
         * @param elem
         * @throws BadLocationException
         * @throws IOException
         */
        protected void emptyTag(Element elem) throws BadLocationException, IOException {
            if (skip && markedElement == elem)
                skip = false;
            else if (!skip)
                super.emptyTag(elem);
        }


        /**
         * Skip elements below an element already written by startTag.
         * Reset the element, if necessary.
         *
         * @param elem
         * @throws IOException
         */
        protected void endTag(Element elem) throws IOException {
            if (skip && markedElement == elem)
                skip = false;
            else if (!skip)
                super.endTag(elem);
        }
    }

    public Document createDefaultDocument() {
        StyleSheet styles = getStyleSheet();
        StyleSheet ss = new EditorStyleSheet();

        ss.addStyleSheet(styles);

        HTMLDocument doc = new EditorDocument(ss);
        doc.setParser(getParser());
        doc.setAsynchronousLoadPriority(-1);
        doc.setTokenThreshold(100);

        return doc;
    }
    
    protected Action getInsertBreakAction() {
        return new EditorActions.InsertBreakAction(editor);
    }
    
    protected Action getTabAction() {
        return new EditorActions.TabAction(editor);
    }
    
    protected Action getBackTabAction() {
        return new EditorActions.BackTabAction(editor);
    }
    
    protected Action getCaretLeftAction() {
        return new EditorActions.CaretLeftAction(editor);
    }
    
    protected Action getCaretRightAction() {
        return new EditorActions.CaretRightAction(editor);
    }

    protected Action getCaretUpAction() {
        return new EditorActions.CaretUpAction(editor);
    }
    
    protected Action getCaretDownAction() {
        return new EditorActions.CaretDownAction(editor);
    }

    public void install(JEditorPane pane) {
        this.pane = pane;
        super.install(pane);

        Keymap defaultMap = pane.getKeymap();
        Keymap map = JEditorPane.addKeymap("editor-keymap", defaultMap);

        EditorActions.setDefaultInsertBreakAction(getAction("insert-break", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
                getInsertBreakAction());

        EditorActions.setDefaultInsertTabAction(getAction("insert-tab", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
                getTabAction());

        EditorActions.setDefaultCaretUpAction(getAction("caret-up", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
                getCaretUpAction());

        EditorActions.setDefaultCaretDownAction(getAction("caret-down", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
                getCaretDownAction());

        EditorActions.setDefaultCaretLeftAction(getAction("caret-backward", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0),
                getCaretLeftAction());

        EditorActions.setDefaultCaretRightAction(getAction("caret-forward", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0),
                getCaretRightAction());

        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK),
                getBackTabAction());

        EditorActions.setDefaultDeletePreviousAction(getAction("delete-previous", pane));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(
                KeyEvent.VK_BACK_SPACE, 0, false),
                new EditorActions.DeletePrevCharAction(editor));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(
                KeyEvent.VK_BACK_SPACE, 0, true),
                new EditorActions.NullAction(editor));
        map.addActionForKeyStroke(KeyStroke.getKeyStroke('\b'),
                new EditorActions.NullAction(editor));

        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK, false),
                editor.getUndoAction());
        map.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK, false),
                editor.getRedoAction());

        pane.setKeymap(map);

        pane.setCaret(new EditorCaret(null, editor));
    }

    public void addActionForKeyStroke(KeyStroke stroke, Action action) {
        Keymap map = editor.getBrowserPane().getKeymap();

        map.addActionForKeyStroke(stroke, action);
    }
    
    class EditorViewFactory extends HTMLFactory implements ViewFactory {

        @Override
        public View create(Element elem) {
            if (componentKit.isComponent(elem))
                return componentKit.create(elem);
  
            return super.create(elem);
       }
        
    }

    @Override
    public ViewFactory getViewFactory() {
        return viewFactory;
    }
    
    
}
